package controllers;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import play.Play;
import play.mvc.Controller;
import play.mvc.results.BadRequest;

import commons.ImageMagickResizer;
import commons.URLSafeBase64Encoder;
import commons.UUIDUtils;

public class ImageController extends Controller {

    private final static Log logger = LogFactory.getLog(ImageController.class);
    
    public static void get(String command) {
        try {
            ImageRequest request = parseParameters(command);
            InputStream is = getFromCache(request);

            if (is == null) {
                is = convertAndCache(request);
            }

            renderBinary(is, null, "image/jpeg", true);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void encode(String id, String command) {
        renderText(encodePath(id, "/" + command));
    }

    private static class ImageRequest {
        final String id, script;
        final List<String> params;

        public ImageRequest(String id, String script, List<String> params) {
            this.id = id;
            this.params = params;
            this.script = script;
        }

        File getCachedFile() {
            StringBuffer cachePath = new StringBuffer(script);
            for (String param : params) {
                cachePath.append('_');
                cachePath.append(param);
            }
            File cacheDir = new File(new File(Play.configuration.getProperty("images.cache.path")), cachePath.toString());
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }

            return new File(cacheDir, id);
        }

        File getOriginalFile() {
            File imageDir = new File(Play.configuration.getProperty("images.path"));
            if (!imageDir.exists()) {
                imageDir.mkdirs();
            }
            return new File(imageDir, id);
        }

        String getUnixCommandLine() {
            // HACK

            StringBuffer buf = new StringBuffer(Play.configuration.getProperty("images.script.path"));
            if (buf.length() > 0 && buf.charAt(buf.length() - 1) != '/') {
                buf.append('/');
            }
            buf.append("convert");
            buf.append(script.substring(0, 1).toUpperCase());
            buf.append(script.substring(1));
            for (String param : params) {
                buf.append(" ");
                buf.append(param);
            }
            return buf.toString();
        }

        @Override
        public String toString() {
            return "ImageRequest [id=" + id + ", script=" + script + ", params=" + params + "]";
        }

    }

    private static InputStream convertAndCache(ImageRequest request) throws Exception {
        File originalFile = request.getOriginalFile();
        if (!originalFile.exists()) {
            notFound();
        }
        FileOutputStream cachedFileOutputStream = new FileOutputStream(request.getCachedFile());
        try {
            FileInputStream fis = new FileInputStream(originalFile);
            // write converted image to cache
            int exitStatus = ImageMagickResizer.convertImage(fis, cachedFileOutputStream, request.getUnixCommandLine());
            if (exitStatus != 0) {
                error();
            }
        } catch (Exception e) {
            // delete cache file (to avoid corrupted file)
            request.getCachedFile().delete();
            throw e;
        }
        // Serve the image from the cache
        return getFromCache(request);
    }

    private static InputStream getFromCache(ImageRequest request) throws FileNotFoundException {
        File image = request.getCachedFile();
        if (!image.exists()) {
            return null;
        }
        return new FileInputStream(image);
    }

    private static ImageRequest parseParameters(String command) {
        try {
            if (org.apache.commons.lang.StringUtils.isBlank(command)) {
                badRequest();
            }

            if (UUIDUtils.isValid(command)) {
                return new ImageRequest(command, "original", Collections.singletonList("jpg"));
            }

            ArrayList<String> params = new ArrayList<String>();
            String[] paramList = decode(command).split("\\/");
            Collections.addAll(params, paramList);

            String image = params.remove(0);
            String script = params.size() > 0 ? params.remove(0) : "original";
            if (params.size() < 1) {
                params.add("jpg");
            }

            return new ImageRequest(image, script, params);
        } catch (Exception e) {
            logger.debug("Wrong request: " + command, e);
            throw new BadRequest();
        }
    }

    private static String decode(String toDecode) throws IllegalArgumentException {
        try {
            Cipher aes = Cipher.getInstance("AES");
            // java only expose 128 bit AES alg ; this is not an issue here.

            SecretKeySpec key = new SecretKeySpec(Play.configuration.getProperty("images.aes.key").getBytes("UTF-8"), "AES");
            aes.init(Cipher.DECRYPT_MODE, key);
            byte decoded[] = aes.doFinal(URLSafeBase64Encoder.decode(toDecode));
            if (decoded.length < 8) {
                throw new IllegalArgumentException("Not a proper encoded uuid");
            }
            UUID u = new UUID(byteArrayToLong(decoded, 0), byteArrayToLong(decoded, 8));
            String command = "";
            if (decoded.length > 16) {
                byte commandBytes[] = new byte[decoded.length - 16];
                System.arraycopy(decoded, 16, commandBytes, 0, commandBytes.length);
                command = new String(commandBytes, "UTF-8");
            }

            return u.toString() + command;
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            throw new RuntimeException(e);
        }
    }

    public static String encodePath(String uuid, String command) {
        try {

            Cipher aes = Cipher.getInstance("AES");
            // java only expose 128 bit AES alg ; this is not an issue here.

            SecretKeySpec key = new SecretKeySpec(Play.configuration.getProperty("images.aes.key").getBytes("UTF-8"), "AES");
            aes.init(Cipher.ENCRYPT_MODE, key);
            UUID u = UUID.fromString(uuid);
            // allocate a large byte buffer (we are sure our data will always
            // fit into it since it must fit into an url)
            ByteBuffer buf = ByteBuffer.allocate(2048);
            buf.put(aes.update(longToByteArray(u.getMostSignificantBits())));
            buf.put(aes.update(longToByteArray(u.getLeastSignificantBits())));
            if (command == null) {
                command = "";
            }
            buf.put(aes.doFinal(command.getBytes("UTF-8")));
            byte[] ret = new byte[buf.position()];
            System.arraycopy(buf.array(), buf.arrayOffset(), ret, 0, ret.length);
            return URLSafeBase64Encoder.encodeBytes(ret);
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            throw new RuntimeException(e);
        }
    }

    // pure fun to write;)
    private static long byteArrayToLong(byte[] array, int offset) {
        long ret = 0;
        for (int i = 0; i < 8; i++) {
            ret <<= 8;
            ret |= array[7 - i + offset] & 0xFF;
        }
        return ret;
    }

    // pure fun to write;)
    private static byte[] longToByteArray(long l) {
        byte[] ret = new byte[8];
        for (int i = 0; i < 8; i++) {
            ret[i] = (byte) (l & 0xFFl);
            l >>>= 8;
        }
        return ret;
    }

    public static void main(String[] args) {
        System.out.println(encodePath("459675c7-7fba-4350-a1af-b6e6dcced398", "/resize-if-bigger/96"));
    }
}
